/*
 * Copyright 2007, 2008 Tiberiumlabs
 *
 * This file is part of Tiberiumlabs Shards.
 *
 * Tiberiumlabs Shards is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Tiberiumlabs Shards is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.tiberiumlabs.logging;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogConfigurationException;
import org.apache.commons.logging.LogFactory;
import org.tiberiumlabs.util.BackgroundWorker;

/**
 *
 * @author <a href="paranoid.tiberiumlabs@gmail.com">Paranoid</a>
 */
public class SingleLog extends LogFactory {

    public static final int NO_LOG = -1;
    public static final int FATAL = 0;
    public static final int ERROR = 1;
    public static final int WARN = 2;
    public static final int INFO = 3;
    public static final int DEBUG = 4;
    public static final int TRACE = 5;

    /**
     * Sets current class as LogFactory implementation, prepares background
     * logging thread and takes current writer as log output.
     * @param logLevel one of <code>NO_LOG, FATAL, ERROR, WARN, INFO, DEBUG, TRACE</code> levels.
     * @param filePath file path for log output. File will be appended if exists and created otherwise.
     * @see #init(int, Writer)
     * @see #NO_LOG
     * @see #FATAL
     * @see #ERROR
     * @see #WARN
     * @see #INFO
     * @see #DEBUG
     * @see #TRACE
     */
    public static final void init(int logLevel, String filePath) {
        init(logLevel, new File(filePath));
    }
    /**
     * Sets current class as LogFactory implementation, prepares background
     * logging thread and takes current writer as log output.
     * @param logLevel one of <code>NO_LOG, FATAL, ERROR, WARN, INFO, DEBUG, TRACE</code> levels.
     * @param file file for log output. File will be appended if exists and created otherwise.
     * @see #init(int, Writer)
     * @see #NO_LOG
     * @see #FATAL
     * @see #ERROR
     * @see #WARN
     * @see #INFO
     * @see #DEBUG
     * @see #TRACE
     */
    public static final void init(int logLevel, File file) {
        try {
            if (!file.exists()) {
                File parent = file.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }
                file.createNewFile();
            }
            init(logLevel, new BufferedWriter(new FileWriter(file, true)));
        } catch (IOException ignore) {
        }
    }
    /**
     * Sets current class as LogFactory implementation, prepares background
     * logging thread and takes current writer as log output.
     * @param logLevel one of <code>NO_LOG, FATAL, ERROR, WARN, INFO, DEBUG, TRACE</code> levels.
     * @param out stream for log output.
     * @see #init(int, Writer)
     * @see #NO_LOG
     * @see #FATAL
     * @see #ERROR
     * @see #WARN
     * @see #INFO
     * @see #DEBUG
     * @see #TRACE
     */
    public static final void init(int logLevel, OutputStream out) {
        init(logLevel, new OutputStreamWriter(out));
    }
    /**
     * Sets current class as LogFactory implementation, prepares background
     * logging thread and takes current writer as log output.
     * @param logLevel one of <code>NO_LOG, FATAL, ERROR, WARN, INFO, DEBUG, TRACE</code> levels.
     * @param out stream for log output.
     * @see #NO_LOG
     * @see #FATAL
     * @see #ERROR
     * @see #WARN
     * @see #INFO
     * @see #DEBUG
     * @see #TRACE
     */
    public static final void init(int logLevel, Writer out) {
        System.setProperty(LogFactory.FACTORY_PROPERTY, SingleLog.class.getName());
        if (logLevel >= 0) {
            LOG_LEVEL = logLevel;
        }
        OUT = out;
        LOG = new LogImpl();
        Thread.setDefaultUncaughtExceptionHandler(LOG);
        LOG.setPriority(Thread.NORM_PRIORITY);
        LOG.setDaemon(true);
        LOG.setLazy(true);
        LOG.setName("BackgroundLoggingThread");
        LOG.start();
    }

    private static int LOG_LEVEL = WARN;
    private static LogImpl LOG;
    private static Writer OUT;

    @Override
    public Log getInstance(Class clazz) throws LogConfigurationException {
        return LOG;
    }

    @Override
    public Log getInstance(String name) throws LogConfigurationException {
        return LOG;
    }

    private static void flush() {
        try {
            if (OUT != null) {
                OUT.flush();
            }
        } catch (Exception ignore) {
        }
    }

    private static void close() {
        flush();
        try {
            OUT.close();
        } catch (IOException ignore) {
        }
    }

    @Override
    protected void finalize() throws Throwable {
        close();
        super.finalize();
    }

    // <editor-fold defaultstate="collapsed" desc=" empty methods ">

    @Override
    public Object getAttribute(String name) {
        return null;
    }

    @Override
    public String[] getAttributeNames() {
        return null;
    }

    @Override
    public void release() {
        close();
    }

    @Override
    public void removeAttribute(String name) {
    }

    @Override
    public void setAttribute(String name, Object value) {
    }

    // </editor-fold>

    private static final class LogMessage {
        private int level;
        private Object message;
        private Throwable throwable;
        private long when;

        public LogMessage(int level, Object message, Throwable throwable, long when) {
            this.level = level;
            this.message = message;
            this.throwable = throwable;
            this.when = when;
        }

        public int getLevel() {
            return level;
        }

        public Object getMessage() {
            return message;
        }

        public Throwable getThrowable() {
            return throwable;
        }

        public long getWhen() {
            return when;
        }

    }

    private static final class LogImpl extends BackgroundWorker<LogMessage> implements Log, Thread.UncaughtExceptionHandler {

        private static final String[] LEVELS_STRINGS = {"FATAL","ERROR","WARN","INFO","DEBUG","TRACE"};

        private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS");
        private static final Date DATE = new Date(0);

        private static final String AT = "    at ";
        private static final String LINE_SEPARATOR = System.getProperty("line.separator");

        @Override
        public void doInBackground(LogMessage e) {

            if (e.getLevel() > LOG_LEVEL) {
                return;
            }
            if (e.getMessage() == null && e.getThrowable() == null) {
                return;
            }

            StringBuilder sb = new StringBuilder();

            // constructing date and level: "[2007.12.31 18:38:41.256] [INFO] "
            sb.append('[');
            DATE.setTime(e.getWhen());
            sb.append(DATE_FORMAT.format(DATE));
            sb.append("] [");
            sb.append(LEVELS_STRINGS[e.getLevel()]);
            sb.append(']').append(' ');

            Object message = e.getMessage();
            if (message != null) {
                sb.append(message);
                sb.append(LINE_SEPARATOR);
            }

            Throwable t = e.getThrowable();
            if (t != null) {
                sb.append("  ");
                sb.append(t);
                sb.append(LINE_SEPARATOR);
                StackTraceElement[] trace = t.getStackTrace();
                if (trace != null && trace.length > 0) {
                    for (int i = 0; i < trace.length; i++) {
                        sb.append(AT).append(trace[i]).append(LINE_SEPARATOR);
                    }
                }
                Throwable cause = t.getCause();
                while (cause != null) {
                    StackTraceElement[] causedTrace = trace;
                    trace = cause.getStackTrace();
                    int m = trace.length - 1;
                    int n = causedTrace.length - 1;
                    while (m >= 0 && n >= 0 && trace[m].equals(causedTrace[n])) {
                        m--;
                        n--;
                    }
                    int framesInCommon = trace.length - 1 - m;
                    sb.append("  Caused by: ");
                    sb.append(cause);
                    sb.append(LINE_SEPARATOR);
                    for (int i = 0; i <= m; i++) {
                        sb.append(AT).append(trace[i]).append(LINE_SEPARATOR);
                    }
                    if (framesInCommon != 0) {
                        sb.append("    ...").append(framesInCommon);
                        sb.append(" more").append(LINE_SEPARATOR);
                    }

                    cause = cause.getCause();
                }
            }

            try {
                OUT.write(sb.toString());
                OUT.flush();
            } catch (Exception ex) {
                ex.printStackTrace(System.err);
            }
        }

        @Override
        protected void finalize() throws Throwable {
            close();
            super.finalize();
        }

        // <editor-fold defaultstate="collapsed" desc=" enabled levels checks ">

        public boolean isDebugEnabled() {
            return LOG_LEVEL >= DEBUG;
        }

        public boolean isErrorEnabled() {
            return LOG_LEVEL >= ERROR;
        }

        public boolean isFatalEnabled() {
            return LOG_LEVEL >= FATAL;
        }

        public boolean isInfoEnabled() {
            return LOG_LEVEL >= INFO;
        }

        public boolean isTraceEnabled() {
            return LOG_LEVEL >= TRACE;
        }

        public boolean isWarnEnabled() {
            return LOG_LEVEL >= WARN;
        }

        // </editor-fold>

        // <editor-fold defaultstate="collapsed" desc=" Log implementation ">

        public void trace(Object message) {
            trace(message, null);
        }

        public void trace(Object message, Throwable t) {
            if (isTraceEnabled()) {
                add(new LogMessage(TRACE, message, t, System.currentTimeMillis()));
            }
        }

        public void debug(Object message) {
            debug(message, null);
        }

        public void debug(Object message, Throwable t) {
            if (isDebugEnabled()) {
                add(new LogMessage(DEBUG, message, t, System.currentTimeMillis()));
            }
        }

        public void info(Object message) {
            info(message, null);
        }

        public void info(Object message, Throwable t) {
            if (isInfoEnabled()) {
                add(new LogMessage(INFO, message, t, System.currentTimeMillis()));
            }
        }

        public void warn(Object message) {
            warn(message, null);
        }

        public void warn(Object message, Throwable t) {
            if (isWarnEnabled()) {
                add(new LogMessage(WARN, message, t, System.currentTimeMillis()));
            }
        }

        public void error(Object message) {
            error(message, null);
        }

        public void error(Object message, Throwable t) {
            if (isErrorEnabled()) {
                add(new LogMessage(ERROR, message, t, System.currentTimeMillis()));
            }
        }

        public void fatal(Object message) {
            fatal(message, null);
        }

        public void fatal(Object message, Throwable t) {
            if (isFatalEnabled()) {
                add(new LogMessage(FATAL, message, t, System.currentTimeMillis()));
            }
        }

        // </editor-fold>

        public void uncaughtException(Thread t, Throwable e) {
            error("Exception in thread " + t.toString(), e);
        }

    }

}
